/*
 * NetworkClassLoader.java
 *
 * Version:
 *      $Id: NetworkClassLoader.java 27 2009-04-17 02:30:39Z PErickson87 $
 *
 * Copyright (c) 2009 Peter O. Erickson
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without major
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, and/or distribute copies of, but
 * under no circumstances sublicense and/or sell, the Software,
 * and to permit persons to whom the Software is furnished to do
 * so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

package edu.rit.poe.cluster;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.URL;
import java.net.UnknownHostException;

/**
 * A class loader that connects to a remote server to load classes.
 * 
 * @author  Peter O. Erickson
 *
 * @version $Id: NetworkClassLoader.java 27 2009-04-17 02:30:39Z PErickson87 $
 */
public class NetworkClassLoader extends ClassLoader {
    
    /** The port upon which the network class loader server is run. */
    public static final int SERVER_PORT = 9873;
    
    /** The hostname of the network class loader server. */
    private InetAddress host;
    
    /**
     * Constructs a new <tt>NetworkClassLoader</tt>.
     * 
     * @param   host                    the hostname of the class-loading server
     *                                  to query from
     * 
     * @throws  UnknownHostException    if the hostname is unknown
     */
    public NetworkClassLoader( String host ) throws UnknownHostException {
        this.host = InetAddress.getByName( host );
    }
    
    /**
     * Loads the class with the specified binary name from across the network.
     * 
     * @param   name                    the binary name of the class to be
     *                                  loaded
     * 
     * @return                          the class with the specified binary name
     * 
     * @throws  ClassNotFoundException  if the class cannot be found, or there
     *                                  is a network error
     */
    @Override
    public Class<?> loadClass( String name ) throws ClassNotFoundException {
        Class retVal = null;
        
        // look for class locally
        try {
            retVal = super.loadClass( name );
        } catch ( Exception e ) {
            // check server for class
            Socket socket = null;
            try {
                socket = new Socket( host, SERVER_PORT );
                
                PrintStream out = new PrintStream( socket.getOutputStream() );
                DataInputStream is =
                        new DataInputStream( socket.getInputStream() );
                
                // send the classname
                out.println( name );
                
                // load the class's bytes
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                byte[] b = new byte[ 512 ];
                int l = 0;
                while ( ( l = is.read( b ) ) != -1 ) {
                    baos.write( b, 0, l );
                }
                byte[] arr = baos.toByteArray();
                
                // define the class
                try {
                    retVal = super.defineClass( name, arr, 0, arr.length );
                } catch ( Exception e2 ) {
                    System.err.println( "Error defining class: " + name );
                    e2.printStackTrace( System.err );
                }
                
            } catch ( IOException ioe ) {
                ioe.printStackTrace();
                throw new ClassNotFoundException();
            }
        }
        return retVal;
    }
    
    /**
     * The serverside of the network class loader.
     * 
     * @author  Peter O. Erickson
     * 
     * @version $Id: NetworkClassLoader.java 27 2009-04-17 02:30:39Z PErickson87 $
     */
    public static class Server extends ClassLoader implements Runnable {
        
        /** The sever socket of the class loader server. */
        private ServerSocket server;
        
        /**
         * Runs the class-loading server.
         */
        public void run() {
            Socket socket = null;
            try {
                server = new ServerSocket( SERVER_PORT );
            } catch ( IOException e ) {
                System.err.println( "Error starting class loader server." );
                return;
            }
            
            // run a loop for connections
            while ( true ) {
                try {
                    try {
                        socket = server.accept();
                    } catch ( SocketException e ) {
                        // the server has been killed
                        break;
                    }
                    
                    BufferedReader in = new BufferedReader(
                            new InputStreamReader( socket.getInputStream() ) );
                    DataOutputStream os =
                            new DataOutputStream( socket.getOutputStream() );
                    
                    // get the requested class name
                    String classname = in.readLine();
                    
                    // write the bytes to the outputstream, or close the conn
                    try {
                        os.write( getBytes( classname ) );
                        os.close();
                    } catch ( Exception e ) {
                        e.printStackTrace();
                        // ignore the error, close the connection
                        os.close();
                    }
                } catch ( IOException e ) {
                    System.err.println( "Client error" );
                    e.printStackTrace( System.err );
                } finally {
                    try {
                        socket.close();
                    } catch ( Exception e ) {
                        // ignore
                    }
                }
            }
        }
        
        /**
         * Returns the <tt>byte</tt>s for a class with the given name.
         * 
         * @param   classname   the binary name of the class to acquire bytes of
         * 
         * @return              the bytes for the specified class
         * 
         * @throws  Exception   if there is any error loading the class
         */
        public byte[] getBytes( String classname ) throws Exception {
            Class clazz = super.loadClass( classname );
            InputStream is = clazz.getClassLoader().getResourceAsStream(
                    ( clazz.getName() ).replaceAll( "\\.", "/" ) + ".class" );
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            
            byte[] b = new byte[ 512 ];
            int l = 0;
            while ( ( l = is.read( b ) ) != -1 ) {
                baos.write( b, 0, l );
            }
            
            return baos.toByteArray();
        }
        
        /**
         * Kills the network class loader server
         */
        public void kill() {
            try {
                server.close();
            } catch ( Exception e ) {
                // ignore
            }
        }
        
    } // Server
    
} // NetworkClassLoader
